#!/usr/bin/env python
import os, sys

class ArgParser(object) :
    def parse(self) :
        import argparse

        def valide_ext_name(f) :
            return f.endswith('.tci')

        class OutDirAction(argparse.Action):
            def __call__(self, parser, namespace, values, option_string=None):
                outdir_abs = os.path.abspath(values)
                if os.path.isdir(outdir_abs) :
                    setattr(namespace, self.dest, outdir_abs)
                else :
                    print '%s %s: directory can not be found!' % (option_string, outdir_abs)
                    parser.print_help()
                    sys.exit(1)

        class FileAction(argparse.Action):
            def __call__(self, parser, namespace, values, option_string=None):
                file_abs = os.path.abspath(values)
                if not valide_ext_name(file_abs) :
                    print '%s %s: invalide tci file extention name, must ends with .tci' % (option_string, file_abs)
                    parser.print_help()
                    sys.exit(1)

                if os.path.isfile(file_abs) :
                    setattr(namespace, self.dest, file_abs)
                    setattr(namespace, 'search', os.path.dirname(file_abs))
                else :
                    print '%s %s: input file does not exist' % (option_string, file_abs)
                    parser.print_help()
                    sys.exit(1)

        parser = argparse.ArgumentParser()
        parser.add_argument('-f', '--file', required=True, action=FileAction)
        parser.add_argument('-o', '--output_dir', default='.', action=OutDirAction)
        parser.add_argument('-g', '--generate', choices=['cpp', 'python', 'java'], default='cpp')

        self.args=parser.parse_args()

    def __getattr__(self, attr) :
        return getattr(self.args, attr)

class IDLDataBase(object) :
    def __init__(self) :
        self.idl_map = {}

    def idl_exists(self, idl_name) :
        return idl_name in self.idl_map

    def append(self, idl_name, idl_obj) :
        if not self.idl_exists(idl_name) :
            self.idl_map[idl_name] = idl_obj

    def set_std_types(self, ts) :
        self.__std_types = ts

    def get_std_types(self) :
        return self.__std_types

    def gen_code(self) :
        for i in self.idl_map.values() :
            i.gen_code()

#global variable
arg_parser = ArgParser()
idl_db = IDLDataBase()

class OrderCounter(object) :
    def __init__(self, init=0) :
        self.__counter = init

    def increase(self) :
        self.__counter += 1
        return self.__counter

class TCOrbElement(object) :
    signature = 'tcorb_base_signature'
    separator = ';'
    indent = ' ' * 4

    def __init__(self, initList, parent=None) :
        self.__id = initList[1]
        self.__init_list = initList
        self.parent = parent

    def get_id(self) :
        return self.__id

    def label(self) :
        return '%s %s' % (self.signature, self.__id)

    def parse(self) :
        raise NotImplementedError()

    def find_symbol(self, symbol) :
        return False

    def check_symbol(self) :
        pass

    @classmethod
    def get_instance(klass, initList, parent) :
        #print 'TCOrbElement get_instance ', klass.__name__
        if initList[0] == klass.signature:
            obj = klass(initList, parent)
            return obj
        else :
            return None


    @staticmethod
    def shift(content, level = 1) :
        rtv=[]
        for i in content :
            tmp = []
            for j in i.split('\n') :
                tmp.append('%s%s' % (TCOrbElement.indent*level, j))
            rtv.append('\n'.join(tmp))
        return rtv

class Import(TCOrbElement) :
    signature = 'import'

    def __init__(self, initList, languang_parser) :
        # initList should like ['import', 'test']
        TCOrbElement.__init__(self, initList, None)
        self.idl= TCOrbIDL(self.get_id())
        self.idl.parse(languang_parser)

    def find_symbol(self, symbol) :
        return self.idl.find_symbol(symbol, True)

    def write_IDL(self):
        self.idl.write_IDL()

    def __str__(self) :
        return '%s "%s.proto"' % (self.signature, self.get_id()) + self.separator + '\n'

class Enum(TCOrbElement) :
    signature = 'enum'

    def __init__(self, initList, parent=None) :
        TCOrbElement.__init__(self, initList, parent)
        self.__members = initList[2:]
        # the enum order starts from 0, put -1 here.
        self.__counter = OrderCounter(-1)

    # the symbol should like 'xxx.yyy', only one full mark. other format should not be matched.
    # because the enum type doesn't have subtype, unlike message.
    def find_symbol(self, symbol) :
        if symbol == self.get_id() :
            return True
        elif symbol.split('.', 1)[0] == self.get_id() :
            return symbol.split('.',1)[1] in self.__members
        else :
            return False

    def __str__(self) :
        return '''\
%s {
%s
}
''' % (self.label(), '\n'.join(TCOrbElement.shift(['%(key)s = %(value)s%(separator)s' % dict(key=k, value=self.__counter.increase(), separator=TCOrbElement.separator) for k in self.__members])))

class ParseMessageEntryError(RuntimeError) :
    pass

class SymbolNotFoundError(RuntimeError) :
    pass

class Message(TCOrbElement) :
    signature = 'message'

    # remove all under score for every members?
    class MessageMember(object) :
        def __init__(self, initList, parent, order_counter) :
            self.__signature = None
            self.__type = None
            self.__identifier = None
            self.__order_counter = order_counter
            self.__default_value = None
            self.parent = parent

            if isinstance(initList[0], list) :
                if len(initList[0]) == 1 :
                    self.__signature = 'optional'
                    self.__type = initList[0][0]
                elif len(initList[0]) == 2 :
                    if initList[0][0] != 'sequence' :
                        raise ParseMessageEntryError('Unsupported message entry type %s' % initList[0][0])
                    else :
                        self.__signature = 'repeated'
                        self.__type = initList[0][1]
                else :
                    raise ParseMessageEntryError('Unexpected message entry format %s' % initList[0])
            else :
                raise ParseMessageEntryError('Unexpected message entry format %s' % initList)

            if len(initList) == 2 :
                self.__identifier = initList[1]

            if len(initList) == 3 :
                self.__identifier = initList[1]
                self.__default_value = initList[2]

        def check_symbol(self) :
            #check symbol, fixme: void type
            if not self.__type in idl_db.get_std_types() :
                #up search until reatching the module
                scope = self.parent
                found = False
                while scope != None :
                    found |= scope.find_symbol(self.__type)
                    if found and self.__default_value :
                        found &= scope.find_symbol(self.__type + '.'+ self.__default_value)
                    if found :
                        break;
                    scope = scope.parent
                if not found :
                    raise SymbolNotFoundError('symbol not found for expression: %s' % self)


        def __str__(self) :
            rtv = '%s %s %s = %s' % (self.__signature, self.__type, self.__identifier, self.__order_counter.increase())
            if self.__default_value:
                rtv += ' [default = %s]' % self.__default_value
            return rtv + TCOrbElement.separator;

        @classmethod
        def get_instance(klass, initList, parent, order_counter=OrderCounter()) :
            if isinstance(initList[0], list) :
                return klass(initList, parent, order_counter)
            else :
                return None

    def __init__(self, initList, parent=None) :
        TCOrbElement.__init__(self, initList)
        self.parent = parent
        self.__counter = OrderCounter()

        #fixme: split the memebers into two kind of groups may cause problem.
        #should put subtype above the message members, in case the message member
        #uses the subtype, then the protobuf may report a error, check later.

        # sub message and enum type
        self.__sub_members = []
        # normal and sequence field
        self.__members = []

        self.parse(initList)

    def append_member(self, m) :
        self.__members.append(m)

    def append_submember(self, m) :
        self.__sub_members.append(m)

    def parse(self, initList) :
        for i in initList[2] :
            m = Message.MessageMember.get_instance(i, self, self.__counter)
            if m is not None :
                self.append_member(m)
            else :
                m = Enum.get_instance(i, self)
                if m is not None :
                    self.append_submember(m)
                else :
                    m = Message.get_instance(i, self)
                    if m is not None :
                        self.append_submember(m)
                    else :
                        raise ParseMessageEntryError('Unexpected message entry format %s' % i)

    def check_symbol(self) :
        for i in self.__members :
            i.check_symbol()
        for i in self.__sub_members :
            i.check_symbol()

    #symbol, full mark seperated field, like xxx.xxx.xxx; could be a type defined by another module
    def find_symbol(self, symbol) :
        found = False
        # remove the message name in the symbol
        # fixme: if the message name is as same as the imported module, the message namespace should
        # overwrite the imported module.

        if symbol == self.get_id() :
            return True

        def f(symbol) :
            for i in self.__sub_members :
                if i.find_symbol(symbol) :
                    return True
            return False

        m_name = None
        s_name = None
        try :
            m_name, s_name = symbol.split('.', 1)
        except :
            m_name = None
            s_name = symbol

        return ((m_name == self.get_id()) and f(s_name)) or f(symbol)

    def __str__(self) :
        body = ''
        if self.__sub_members :
            sub_mem_body = '%s' % ('\n'.join(TCOrbElement.shift([str(i) for i in self.__sub_members])))
            body += sub_mem_body + '\n'
        if self.__members :
            mem_body = '%s' % ('\n'.join(TCOrbElement.shift([str(i) for i in self.__members])))
            body += mem_body + '\n'

        return '''\
%s {
%s
}
''' % (self.label(), body)

class ParameterFormatError(RuntimeError) :
    pass

class Interface(TCOrbElement) :
    signature = 'interface'

    class InterfaceMethod(object) :
        def __init__(self, initList) :
            self.__returns, self.__id, raw_parameter = initList

            class Parameter(object) :
                def __init__(self, initList) :
                    self.direction = 'in'
                    if len(initList) == 1 :
                        self.type = initList[0]
                    elif len(initList) == 2 :
                        if initList[0] == 'out' :
                            self.direction = 'out'
                            self.type = initList[1]
                        else :
                            raise ParameterFormatError(str(initList))
                    else :
                        raise ParameterFormatError(str(initList))

                def __str__(self) :
                    return '%s %s' % (self.direction, self.type)

            self.__parameters = []
            for i in raw_parameter :
                self.__parameters.append(Parameter(i))

        def get_id(self) :
            return self.__id

        def get_return(self) :
            return self.__returns

        def get_parameters(self) :
            return self.__parameters

        def get_required_symbols(self) :
            return [self.__returns] + [ i.type for i in self.__parameters]

        def __str__(self) :
            return '%(returns)s %(id)s(%(parameters)s); ' % dict(id=self.__id, parameters=', '.join(str(i) for i in self.__parameters), returns=self.__returns)

    def __init__(self, initList, parent) :
        TCOrbElement.__init__(self, initList)
        self.__method_list=[]
        self.parent = parent

        for i in initList[2:] :
            m = Interface.InterfaceMethod(i)
            self.__method_list.append(m)

    def check_symbol(self) :
        s_list = []
        for i in self.__method_list :
            s_list.extend(i.get_required_symbols())
        s_list=list(set(s_list))
        for i in s_list :
            if not i in idl_db.get_std_types() :
                # the parent should be the idl module object
                if not self.parent.find_symbol(i) :
                    raise SymbolNotFoundError('symbol not found for: %s in %s' % (i, self))

    def __str__(self) :
        return '''\
%s {
%s
}''' % (self.label(), '\n'.join(TCOrbElement.shift([str(i) for i in self.__method_list])))

    def gen_cpp_code(self) :
        pass

class CodeTypeError(RuntimeError) :
    pass

class ExtFileNameError(RuntimeError) :
    pass

class IDLFileNotFound(RuntimeError) :
    pass

class OutputDirError(RuntimeError) :
    pass

class ParseIDLError(RuntimeError) :
    pass

class TCOrbIDL(object) :
    def __init__(self, idl_file) :
        self.__idl_file = idl_file
        #the parent should be always None.
        self.parent = None

        # fixme, support python java later
        self.__supported_code = ['cpp']
        self.__element_list = []

        self.__check_parameter()

    def __check_parameter(self) :
        # locate IDL file
        self.__IDL_full_path = ''
        abs_path = os.path.abspath(self.__idl_file)
        # if the IDL file is the first to process,
        if os.path.isfile(abs_path) :
            self.__IDL_full_path = abs_path
        # the IDL file is just a imported name, without path and ext
        # the search dir should be used.
        else :
            abs_path = os.path.abspath(os.path.join(arg_parser.search, self.__idl_file + '.tci'))
            if os.path.isfile(abs_path) :
                self.__IDL_full_path = abs_path
            else :
                raise IDLFileNotFound('the IDL file %s can not be found' % abs_path)

        # code generation
        if arg_parser.generate not in self.__supported_code :
            raise CodeTypeError('%s is not in language support list: %s' % ','.join(arg_parser.generate, self.__supported_code))

        # now the self.__IDL_full_path should point to the real file with ext name '.tci'
        # make sure the first IDL file to process ends with '.tci'

        # remove ext name -- '.tci'
        self.module_name = os.path.basename(self.__IDL_full_path)[0:-4]
        module_name = self.module_name
        # protobuf file ends with .proto
        self.pb_path = os.path.join(arg_parser.output_dir, module_name + '.proto')
        self.interface_path = os.path.join(arg_parser.output_dir, module_name + '.tcii')

        self.pb_include_filename = '.'.join([module_name, 'pb', 'h'])
        self.pb_cpp_filename = '.'.join([module_name, 'pb', 'cc'])
        self.interface_include_filename = '.'.join([module_name, 'i', 'h'])
        self.interface_cpp_filename = '.'.join([module_name, 'i', 'cc'])

    def parse(self, languang_parser) :
        if not idl_db.idl_exists(self.module_name) :
            #in case the multi-level imported module refer to the current one, we add it first.
            idl_db.append(self.module_name, self)
            self.__element_list = []
            parsed_result = languang_parser.parse_str(file(self.__IDL_full_path, 'r').read())
            for i in parsed_result :
                m = Import.get_instance(i, languang_parser) or\
                    Enum.get_instance(i, self) or\
                    Message.get_instance(i, self) or\
                    Interface.get_instance(i, self)
                if not m :
                    # very rare, if it passed the EBNF parser, it should could be parsed successfully
                    raise ParseIDLError('error occured when parsing segment %s' % i)

                self.__element_list.append(m)

            for i in self.__element_list :
                i.check_symbol()

    def find_symbol(self, symbol, package_prefix_only=False) :
        #package_prefix_only flag is for other modules lookup symbol in this module
        def f(symbol) :
            for i in self.__element_list :
                if i.find_symbol(symbol) :
                    return True
            return False

        m_name = None
        s_name = None
        try :
            m_name, s_name = symbol.split('.', 1)
        except :
            m_name = None
            s_name = symbol

        if package_prefix_only :
            return ((m_name == self.module_name) and f(s_name))
        else :
            return ((m_name == self.module_name) and f(s_name)) or f(symbol)

    def get_message_header(self) :
        return self.pb_include_filename

    def get_message_source(self) :
        return self.pb_cpp_filename

    def get_interface_header(self) :
        return self.interface_include_filename

    def get_interface_source(self) :
        return self.interface_cpp_filename

    def write_IDL(self) :
        from StringIO import StringIO
        pb_sstrm = StringIO()
        interface_sstrm = StringIO()

        pkg_directive = 'package %s;\n' % self.module_name
        pb_sstrm.write(pkg_directive)
        interface_sstrm.write(pkg_directive)

        for i in self.__element_list :
            #write module name as package directive

            if i.signature == 'import' :
                pb_sstrm.write(str(i))
                interface_sstrm.write(str(i))
                i.write_IDL()
            if i.signature in ['enum', 'message'] :
                pb_sstrm.write(str(i))
            if i.signature == 'interface' :
                interface_sstrm.write(str(i))

        pb_ofstream = file(self.pb_path, 'w')
        pb_ofstream.write(pb_sstrm.getvalue())
        pb_ofstream.close()

        interface_ostream = file(self.interface_path, 'w')
        interface_ostream.write(interface_sstrm.getvalue())
        interface_ostream.close()

    def gen_code(self) :
        import subprocess
        # for protobuf
        try :
            subprocess.check_output('protoc %(pb)s -I%(out_dir)s --%(language)s_out=%(out_dir)s' % dict(pb=self.pb_path, language=arg_parser.generate, out_dir=arg_parser.output_dir))
        except Exception, e:
            raise
            print str(e)
            sys.exit(1)
        else :
            print 'run protoc on %s successfully!' % self.__IDL_full_path

class TCIParser(object) :
    def __initParser(self) :
        from pyparsing import  Word, alphas, alphanums, Regex, Suppress, Forward, Group, oneOf, ZeroOrMore, Optional, delimitedList, Keyword, restOfLine, quotedString
        kwds = """message sequence enum true false import interface void"""
        for kw in kwds.split():
            exec("%s_ = Keyword('%s')" % (kw.upper(), kw))

        self.types = """double float int32 int64 uint32 uint64 sint32 sint64
                   fixed32 fixed64 sfixed32 sfixed64 bool string bytes void"""

        ident = ~oneOf(kwds) + Word(alphas+"_",alphanums+"_").setName("identifier")
        integer = Regex(r"[+-]?\d+")

        LBRACE,RBRACE,LBRACK,RBRACK,LPAR,RPAR,LABRACK,RABRACK,EQ,SEMI = map(Suppress,"{}[]()<>=;")

        messageBody = Forward()

        messageDefn = MESSAGE_ - ident + LBRACE + messageBody + RBRACE

        typespec = oneOf(self.types) | ident
        comma_seperated_typespec = delimitedList(typespec, '.', combine=True)

        rvalue = integer | TRUE_ | FALSE_ | ident
        # fieldDefn ::= type ident {'=' rvalue}?
        fieldDefn = (Group(comma_seperated_typespec| (SEQUENCE_ + LABRACK + comma_seperated_typespec + RABRACK)) + ident("ident") + Optional(EQ + rvalue) + SEMI)

        # enumDefn ::= 'enum' ident '{' { ident ';' }* '}'
        enumDefn = ENUM_ - ident + LBRACE + ZeroOrMore( ident + SEMI ) + RBRACE

        # messageBody ::= { fieldDefn | enumDefn | messageDefn }*
        messageBody << Group(ZeroOrMore( Group(fieldDefn | enumDefn | messageDefn ) ))

        # interfaceMethodDefn ::= rtv ident '(' {'out'}? typespec [',' {'out'}? typespec ]* ')';
        paravalue = Group(VOID_ | delimitedList(Group(Optional(Word('out')) + comma_seperated_typespec), ','))
        returnvalue = VOID_ | comma_seperated_typespec
        interfaceMethodDefn = Group(returnvalue + ident + LPAR + paravalue + RPAR + SEMI)

        # interfaceDirective ::= 'interface' ident '{' interfaceMethodDefn* '}'
        interfaceDirective = INTERFACE_ - ident + LBRACE + ZeroOrMore(interfaceMethodDefn) + RBRACE

        comment = '#' + restOfLine

        importDirective = IMPORT_ - Word(alphas+"_",alphanums+"_") + SEMI

        topLevelStatement = Group(messageDefn | enumDefn | interfaceDirective)

        self.parser = ZeroOrMore(Group(importDirective)) + ZeroOrMore(topLevelStatement)

        self.parser.ignore(comment)

    def __init__(self) :
        self.__initParser()
        self.__predefined_types_for_interface = self.types.split()
        idl_db.set_std_types(self.types.split())

    def parse_str(self, tci_str) :
        return self.parser.parseString(tci_str, parseAll=True).asList()

if __name__ == '__main__' :
    parser = TCIParser()
    arg_parser.parse()
    idl = TCOrbIDL(arg_parser.file)
    idl.parse(parser)
    idl.write_IDL()
    idl.gen_code()


